# -*- coding: utf-8 -*-
import os
import logging

import zmq.green as zmq
from gevent import Timeout
from gevent import Greenlet
from gevent.lock import Semaphore
import gevent

from alphabet import *

ctx = zmq.Context()


class DemandClient(object):

    def __init__(self):
        _sock = ctx.socket(zmq.REQ)
        _sock.connect("tcp://%s:%d" % (HOST, REQ_PORT))
        self._sock = _sock

    def send(self, command):
        """
        :param command: COMMAND or [COMMAND, ...(extra data)]
        """
        if isinstance(command, str):
            self._sock.send(command)
        else:
            command = [i if isinstance(i, str) else str(i) for i in command]
            self._sock.send_multipart(command)

    def recv(self):
        """
        :return [RESULT, ...(extra data)]
        """
        return self._sock.recv_multipart()


class DemandPool(object):

    def __init__(self):
        """
        redis's ConnectionPool default max_connection is 2**31
        so nearly worry about way us Pool haven't max_size
        """
        self._idle_clients = []
        self._busy_clients = []

    def get_client(self):
        try:
            client = self._idle_clients.pop()
        except IndexError:
            # client = self.make_client()
            client = DemandClient()
        self._busy_clients.append(client)
        return client

    # def make_client(self):
    #     if self._current_size >= self.max_size:
    #         raise Exception("Too many connections")
    #     self._current_size += 1
    #     return DemandClient()

    def release(self, client):
        self._busy_clients.remove(client)
        self._idle_clients.append(client)


class Demand(object):

    def __init__(self, timeout=DEMANDTIMEOUT, log=None):
        if not log:
            log = logging.getLogger(__name__)
            console = logging.StreamHandler()
            console.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s'))
            console.setLevel(logging.DEBUG)
            log.addHandler(console)
            log.setLevel(logging.DEBUG)
        self.pool = DemandPool()
        self.log = log
        self.timeout = timeout

    def _request(self, command):
        """
        command is list like [COMMAND, deviceId, ...(extra data)]
        1.request --> broker
        2.broker  --> dispose
        3.dispose --> broker
        4.broker --> request
        """
        client = self.pool.get_client()
        client.send(command)
        try:
            with Timeout(self.timeout):
                result = client.recv()
        except Timeout:
            # 正常不应该超时,只有与broker通信缓慢时出现
            self.log.error('happen in PID:%d' % os.getpid(), exc_info=True)
            self.pool._busy_clients.remove(client)
            return [TIMEOUT]
        else:
            self.pool.release(client)
            return result

    def send(self, imei, instruct, params):
        if isinstance(params, list):
            command = [SENDBLOCK, imei, instruct]
            command[3:] = params
            return self._request(command)
        else:
            return self._request([SENDBLOCK, imei, instruct, params])

    def send_nowait(self, imei, instruct, params):
        if isinstance(params, list):
            command = [SENDNOBLOCK, imei, instruct]
            command[3:] = params
            return self._request(command)
        else:
            return self._request([SENDNOBLOCK, imei, instruct, params])

    def find(self, imei):
        return self._request([FINDONE, imei])

    def getlist(self, page, num):
        return self._request([GETLIST, str(page), str(num)])

    def gettotal(self):
        return self._request([GETTOTAL])


class Dispose(object):

    def __init__(self, resource, heartbeat=HEARTBEATTIME, interact=None, interact_noblock=None):
        """
        broker default ttl 15s,us heartbeat 10s
        resource are Dict to find device,is Big Dict!
        interact,interact_noblock function will like:

        def interact(imei, instruct, params):
            if imei not in resource:
                return NO
            try:
                ...
                return OK
            except Timeout:
                return TIMEOUT

        def interact_noblock(imei, instruct, params):
            if imei in resource:
                ...
            return None
        """
        if not isinstance(resource, dict):
            raise RuntimeError('Dispose resource need Dict to find Device')
        if not callable(interact):
            raise RuntimeError('interact must be callable')
        if not callable(interact_noblock):
            raise RuntimeError('interact_noblock must be callable')

        _sock = ctx.socket(zmq.REQ)
        _sock.connect("tcp://%s:%d" % (HOST, RES_PORT))
        self._sock = _sock

        sub = ctx.socket(zmq.SUB)
        sub.connect("tcp://%s:%d" % (HOST, CHA_PORT))
        sub.setsockopt(zmq.SUBSCRIBE, '')
        self._sub = sub
        self.heartbeat = heartbeat
        self.lock = Semaphore(1)
        self.resource = resource
        self.interact = interact
        self.interact_noblock = interact_noblock

    def _handle(self, message):
        # print message
        client_id = message[0]
        instruct = message[2]
        data = message[3:]
        if instruct == SENDBLOCK:
            imei = data[0]
            toinstruct = data[1]
            params = data[2:]
            if imei in self.resource:
                try:
                    with Timeout(DISPOSETIMEOUT):
                        result = self.interact(imei, toinstruct, params)
                        # result must be OK NO TIMEOUT
                        self.send([instruct, client_id, result])
                except Timeout:
                    self.send([instruct, client_id, TIMEOUT])
            else:
                self.send([instruct, client_id, NO])
            return None
        if instruct == SENDNOBLOCK:
            imei = data[0]
            toinstruct = data[1]
            params = data[2:]
            if imei in self.resource:
                self.interact_noblock(imei, toinstruct, params)
                self.send([instruct, client_id, OK])
            else:
                self.send([instruct, client_id, NO])
            return None
        if instruct == GETLIST:
            page = int(data[0])
            num = int(data[1])
            devicelist = self.resource.keys()[page*num:(page+1)*num]
            respond = [instruct, client_id]
            respond.extend(devicelist)
            self.send(respond)
            return None
        if instruct == GETTOTAL:
            self.send([instruct, client_id, str(len(self.resource))])
            return None
        if instruct == FINDONE:
            if data[0] in self.resource:
                self.send([instruct, client_id, OK])
            else:
                self.send([instruct, client_id, NO])
            return None

    def handle_publish(self):
        while 1:
            message = self._sub.recv_multipart()
            Greenlet.spawn(self._handle, message).start()

    def send(self, message):
        with self.lock:
            self._sock.send_multipart(message)
            result = self._sock.recv_multipart()
        return result

    def loop(self):
        while 1:
            self.send([HEARTBEAT])
            gevent.sleep(self.heartbeat)

    def start(self):
        Greenlet.spawn(self.handle_publish).start()
        Greenlet.spawn(self.loop).start()
